/**
 * Copyright 2008 Google Inc.
 * Copyright 2008 IGN Entertainment Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @fileoverview This file implements a google gears backed container. It's a port of
 * in memory mock container provided by google. This container is useful for
 * development, prototyping and testing. The advantage of using google gears backed
 * container over mock container is its ability to show shared data between two browser
 * windows which is not possible in case of mock container.
 *
 */
var happyhour = function() {};

/**
 * Implements the opensocial.Container apis by using google gears as persistence store
 *
 * @param appId application id.
 * @param instanceId application id.
 * @param viewerId Person object that corresponds to the viewer.
 * @param ownerId Person object that corresponds to the owner.
 * @constructor
 */
//assuming google gears
opensocial.HappyHourContainer = function(appId, instanceId, viewerId, ownerId, bootstrap) {
    this.viewerId = viewerId;
    this.ownerId = ownerId;
    this.instanceId = instanceId;
    this.appId = appId;

    var db = google.gears.factory.create('beta.database', '1.0');
    db.open('HappyHour-0.7');
    this.db = db;
    db.execute('begin');
    try {
        this.initSchema(db);
        if (bootstrap) {
            bootstrap(db);
        }
        this.loadOwner();
        this.loadViewer();
        db.execute('commit');
    }
    catch(ex) {
        db.execute('rollback');
        throw ex;
    }
    
    //TODO uncomment this
    opensocial.Container.call(this); 
};

opensocial.HappyHourContainer.inherits(opensocial.Container);

/**
 * @private
 */
happyhour.Query = function(db, q, params) {
	this.db = db;
	this.q = q;
	this.params = params;
};

/**
 * @private
 */
happyhour.Query.Constants = {BREAK: 1};

/**
 * @private
 */
happyhour.Query.prototype.scalar = function(defaultValue) {
  var rs = this.db.execute(this.q, this.params);
	try {
		return rs.isValidRow() ? rs.field(0) : defaultValue; 
	}
  finally {
    rs.close();
  }
};

/**
 * @private
 */
happyhour.Query.prototype.singleton = function(cursorCallback, defaultValue) {
  var rs = this.db.execute(this.q, this.params);
	try {
		return rs.isValidRow() ? cursorCallback(rs) : defaultValue; 
	}
  finally {
    rs.close();
  }
};

/**
 * @private
 */
happyhour.Query.prototype.map = function(cursorCallback) {
  var rs = this.db.execute(this.q, this.params);
	try {
		while(rs.isValidRow()) {
			if(cursorCallback(rs) == happyhour.Query.Constants.BREAK) {
				break;
			} 
			rs.next();
		}
	}
  finally {
    rs.close();
  }
};

/**
 * @private
 */
happyhour.query = function(db, q, params) {
	return new happyhour.Query(db, q, params);
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.initSchema = function(db) {
    db.execute(
            "CREATE TABLE if not exists [person] (\n" +
            "[id] integer not null, \n" +
            "[name] text, \n" +
            "[thumbnail_url] text, \n" +
            "[profile_url] text,\n" +
            "[dob] timestamp,\n" +
            "[gender] text, \n" +
            "[email] text, \n" +
            "[extended_properties] text,\n" +
            "[last_updated_on] timestamp,\n" +
            "[created_on] timestamp DEFAULT CURRENT_TIMESTAMP not null,\n" +
            "primary key([id])\n" +
            "   check ([gender] in ('M', 'F'))\n" +
            ")"
            );

    db.execute(
            "CREATE TABLE if not exists [activity] (\n" +
            "   [id] integer not null, \n" +
            "   [app_id] integer  NOT NULL,\n" +
            "   [user_id] integer not null,\n" +
            "   [stream_title] text not null,\n" +
            "   [stream_favicon_url] text,\n" +
            "   [stream_source_url] text,\n" +
            "   [stream_url] text,\n" +
            "   [title] text not null,\n" +
            "   [summary] text,\n" +
            "   [body] text,\n" +
            "   [external_id] text,\n" +
            "   [url] text,\n" +
            "   [posted_time] timestamp,\n" +
            "   [extended_properties] text,\n" +
            "   [created_on] timestamp DEFAULT CURRENT_TIMESTAMP not null,\n" +
            "   [priority] text,\n" +
            "   primary key([id])\n" +
            "   check ([priority] in ('HIGH', 'LOW'))\n" +
            ")"
            );

    db.execute(
            "CREATE INDEX IF NOT EXISTS [activity_stream_index] on [activity]([app_id], [user_id], [stream_title])"
            );

    db.execute(
            "CREATE TABLE if not exists [activity_media_items] (\n" +
            "   [id] integer not null,\n" +
            "   [activity_id] integer not null,\n" +
            "   [mime_type] text not null,   \n" +
            "   [type] text not null,   \n" +
            "   [url]  text url not null,\n" +
            "   [extended_properties] text,\n" +
            "   primary key([id]),\n" +
            "   check ([type] in ('audio', 'image', 'video')) \n" +
            ")"
            );

    db.execute(
            "CREATE TABLE if not exists [friends] (\n" +
            "[person_id] integer  NOT NULL,\n" +
            "[friend_id] integer  NOT NULL,\n" +
            "PRIMARY KEY ([person_id],[friend_id])\n" +
            ")"
            );

    db.execute(
            "CREATE TABLE  if not exists [global_app_data] (\n" +
            "[app_id] integer  NOT NULL,\n" +
            "[key_name] text  NOT NULL,\n" +
            "[value]   NULL,\n" +
            "PRIMARY KEY ([app_id],[key_name])\n" +
            ")"
            );

    db.execute(
            "CREATE TABLE  if not exists [instance_app_data] (\n" +
            "[app_id] integer  NOT NULL,\n" +
            "[instance_id] integer  NOT NULL,\n" +
            "[key_name] text  NOT NULL,\n" +
            "[value]   NULL,\n" +
            "PRIMARY KEY ([app_id],[instance_id],[key_name])\n" +
            ")"
            );

    db.execute(
            "CREATE TABLE  if not exists [person_app_data] (\n" +
            "[app_id] integer  NOT NULL,\n" +
            "[person_id] integer  NOT NULL,\n" +
            "[key_name] text  NOT NULL,\n" +
            "[value]   NULL,\n" +
            "PRIMARY KEY ([app_id],[person_id],[key_name])\n" +
            ")"
            );

    db.execute(
            "CREATE TRIGGER  if not exists [friends_del_trg] \n" +
            "AFTER DELETE ON [friends] \n" +
            "FOR EACH ROW \n" +
            "BEGIN \n" +
            "delete from friends where\n" +
            "person_id = old.friend_id\n" +
            "and\n" +
            "friend_id = old.person_id;\n" +
            "END"
            );

    db.execute(
            "CREATE TRIGGER if not exists [friends_ins_trg] \n" +
            "AFTER INSERT ON [friends] \n" +
            "FOR EACH ROW \n" +
            "BEGIN \n" +
            "insert or ignore into friends (person_id, friend_id)\n" +
            " values (new.friend_id, new.person_id);\n" +
            "END"
            );

    db.execute(
            "CREATE TRIGGER  if not exists [friends_upd_trg] \n" +
            "AFTER UPDATE ON [friends] \n" +
            "FOR EACH ROW WHEN OLD.PERSON_ID <> NEW.PERSON_ID OR OLD.FRIEND_ID <> NEW.FRIEND_ID \n" +
            "BEGIN \n" +
            "update friends set\n" +
            "person_id = new.friend_id,\n" +
            "friend_id = new.person_id\n" +
            "where\n" +
            "person_id = old.friend_id\n" +
            "and\n" +
            "friend_id = old.person_id;\n" +
            "END"
            );
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.loadOwner = function() {
    this.owner = this.getPerson(this.ownerId) || this.createOwner();
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.loadViewer = function() {
    this.viewer = this.getPerson(this.viewerId) || this.createViewer();
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.createOwner = function() {
    var db = this.db;
    db.execute("insert or ignore into person (id, name, email) values (?, ?, ?)", [this.ownerId || null, 'Friendly Host'   , 'friendly.host@example.com']);
    this.ownerId = db.lastInsertRowId;
    return this.getPerson(this.ownerId);
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.createViewer = function() {
    var db = this.db;
    db.execute("insert or ignore into person (id, name, email) values (?, ?, ?)", [this.viewerId || null, 'Honerable Guest' , 'honerable.guest@example.com']);
    this.viewerId = db.lastInsertRowId;
    return this.getPerson(this.viewerId);
};

/**
 * @private
 */
opensocial.HappyHourContainer.PERSON_COLUMNS = " id, name, thumbnail_url, profile_url, dob, gender, email, extended_properties ";

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.getPerson = function(personId) {
    return happyhour.query(this.db, "select "+ opensocial.HappyHourContainer.PERSON_COLUMNS + 
       " from person where id = ?", [personId]).singleton(this.materializePerson);
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.getFriends = function(personId) {
    var db = this.db;
	//unfortunately bind parameter doesn't work with nullif
    //var rs = db.execute("select id, name, email, thumbnail_url, profile_url from person where id in(select ifnull(nullif(person_id, "+personId+"), friend_id) from friends where person_id = ? or friend_id = ?)", [personId, personId]);
    //but works with case
    /*
       create unique index friends_index on friends( min(person_id, friend_id), max(person_id, friend_id) );

       we would like to enforce constraints on friends to avoid symmetric relationship
       since sqlite doesn't support function based indexes we can only do (person_id, friend_id) as pk
       and triggers make sure that pair exits. so to find friends we don't run this query anymore
       (select (case person_id when ? then friend_id else person_id end) from friends where person_id = ? or friend_id = ?)
     */
    var q = "select "+ opensocial.HappyHourContainer.PERSON_COLUMNS +" from person where id in" +
                        "(select friend_id from friends where person_id = ?)";
    var params = [personId];
    var me = this;
     
    var a = new Array();
    happyhour.query(this.db, q, params).map(function(rs) {
        a.push(me.materializePerson(rs));
    });
    return a;
};

/**
 * 
 */
opensocial.HappyHourContainer.prototype.requestCreateActivity = function(activity, priority, opt_callback) {
    var db = this.db;
    // Permissioning is not being handled in the sample container. All real
    // containers should check for user permission before posting activities.
    var userId = this.viewer.getId();
    var appId = this.appId;
    db.execute('begin');
    try {
        activity.setField(opensocial.Activity.Field.USER_ID, userId);
        activity.setField(opensocial.Activity.Field.APP_ID, this.appId);
        if (activity.getField(opensocial.Activity.Field.POSTED_TIME) == null) {
            activity.setField(opensocial.Activity.Field.POSTED_TIME, new Date());
        }
        var postedTime = this.toISO8601String(activity.getField(opensocial.Activity.Field.POSTED_TIME));
        db.execute("INSERT into activity (id, app_id, user_id, stream_title, stream_favicon_url, stream_source_url, stream_url, \n"+ 
             " title, summary, body, external_id, url, posted_time, extended_properties, priority " +
                   ") VALUES (null, ?, ?, ?, ?, ?, ?,\n"+
                   " ?, ?, ?, ?, ?, ?, ?, ?)",
                [appId, userId,
                    activity.getField(opensocial.Activity.Field.STREAM_TITLE) || null,
                    activity.getField(opensocial.Activity.Field.STREAM_FAVICON_URL) || null,
                    activity.getField(opensocial.Activity.Field.STREAM_SOURCE_URL) || null,
                    activity.getField(opensocial.Activity.Field.STREAM_URL) || null,
                    activity.getField(opensocial.Activity.Field.TITLE) || null,
                    activity.getField(opensocial.Activity.Field.SUMMARY) || null,
                    activity.getField(opensocial.Activity.Field.BODY) || null,
                    activity.getField(opensocial.Activity.Field.EXTERNAL_ID) || null,
                    activity.getField(opensocial.Activity.Field.URL) || null,
                    postedTime,
                    null,
                    priority
                ]);
        var activityId = db.lastInsertRowId;
        activity.setField(opensocial.Activity.Field.ID, activityId);

        var mediaItems = activity.getField(opensocial.Activity.Field.MEDIA_ITEMS);
        if (mediaItems) {
            for (var i = 0; i < mediaItems.length; i++) {
                var mediaItem = mediaItems[i];
                db.execute("INSERT into activity_media_items (id, activity_id, mime_type, type, url, extended_properties" +
                           ") VALUES (null, ?, ?, ?, ?, ?)",
                        [activityId,
                            mediaItem.getField(opensocial.Activity.MediaItem.Field.MIME_TYPE) || null,
                            mediaItem.getField(opensocial.Activity.MediaItem.Field.TYPE) || null,
                            mediaItem.getField(opensocial.Activity.MediaItem.Field.URL) || null,
                            null
                        ]);
            }
        }
        db.execute('commit');
    }
    catch(ex) {
        alert("Error while creating activity "+ex.message);
        db.execute('rollback');
        throw ex;
    }

    if (opt_callback) {
        opt_callback();
    }
};

/**
 * 
 */
opensocial.HappyHourContainer.prototype.newActivityMediaItem = function(mimeType, url, opt_params) {
	if(opt_params && opt_params.type) {
        return new opensocial.Activity.MediaItem(mimeType, url, opt_params);
	}
	else {
		var m = new opensocial.Activity.MediaItem(mimeType, url, opt_params);
		m.setField(opensocial.Activity.MediaItem.Field.TYPE, mimeType);
		return m;
	}
};

/**
 * @private
 */
opensocial.HappyHourContainer.isArray = function(object) {
    return object && object.constructor === Array;
};

/**
 * @private
 */
opensocial.HappyHourContainer.isString = function(object) {
    return object && object.constructor === String;
};

/**
 * @private
 */
opensocial.HappyHourContainer.keyNameClause = function(keys) {
    var keyClause = null;
    if (opensocial.HappyHourContainer.isArray(keys)) {
        if (keys.length == 1 && "*" != keys[0]) {
            keyClause = {sql: " key_name = ? ", params: keys};
        }
        else {
            keyClause = {sql: " key_name in (" + keys.join("?") + ") ", params: keys};
        }
    }
    if (opensocial.HappyHourContainer.isString(keys) && "*" != keys) {
        keyClause = {sql: " key_name = ? ", params: [keys]};
    }
    return keyClause || {sql: " ", params: []};
};

/**
 * This method returns the data requested about the viewer and his/her friends.
 *
 * To keep this simple (for now), the PeopleRequestFields values such as sort
 * order, filter, pagination, etc. specified in the DataRequest are ignored and
 * all requested data is returned in a single call back.
 *
 * @param {Object} dataRequest The object that specifies the data requested.
 * @param {Function} callback The callback method on completion.
 */
opensocial.HappyHourContainer.prototype.requestData = function(dataRequest,
                                                               callback) {
    var requestObjects = dataRequest.getRequestObjects();
    var dataResponseValues = {};
    var globalError = false;

    for (var requestNum = 0; requestNum < requestObjects.length; requestNum++) {
        var request = requestObjects[requestNum].request;
        var requestName = requestObjects[requestNum].key;
        var requestedValue;
        var hadError = false;

        switch (request.type) {
            case 'FETCH_PERSON' :
                var personId = request.id;
                if (personId == opensocial.DataRequest.PersonId.VIEWER) {
                    requestedValue = this.viewer;
                } else if (personId == opensocial.DataRequest.PersonId.OWNER) {
                    requestedValue = this.owner;
                } else {
                    requestedValue = this.getPerson(personId);
                }
                break;

            case 'FETCH_PEOPLE' :
                var idSpec = request.idSpec;
                var persons = [];
                if (idSpec == opensocial.DataRequest.Group.VIEWER_FRIENDS) {
                    persons = this.getFriends(this.viewer.getId());
                } else if (idSpec == opensocial.DataRequest.Group.OWNER_FRIENDS) {
                    persons = this.getFriends(this.owner.getId());
                } else {
                    if (!opensocial.Container.isArray(idSpec)) {
                        idSpec = [idSpec];
                    }
                    for (var i = 0; i < idSpec.length; i++) {
                        var person = this.getPerson(idSpec[i]);
                        if (person != null) {
                            persons.push(person);
                        }
                    }
                }
                requestedValue = new opensocial.Collection(persons);
                break;

            case 'FETCH_GLOBAL_APP_DATA' :
                var keyClause = opensocial.HappyHourContainer.keyNameClause(request.keys);
                var q = "select key_name, value from global_app_data where " + keyClause.sql +
                        " and app_id =  ? ";
                var values = {};
                happyhour.query(this.db, q, keyClause.params.concat([this.appId])).map(function(rs) {
                  values[rs.field(0)] = rs.field(1);
                });
                requestedValue = values;
                break;

            case 'FETCH_INSTANCE_APP_DATA' :
                var keyClause = opensocial.HappyHourContainer.keyNameClause(request.keys);
                var q = "select key_name, value from instance_app_data where " + keyClause.sql +
                        " and app_id =  ? and instance_id = ?";
                var values = {};
                happyhour.query(this.db, q, keyClause.params.concat([this.appId, this.instanceId])).map(
                  function(rs) {
                    values[rs.field(0)] = rs.field(1);
                  }
                );
                break;

            case 'UPDATE_INSTANCE_APP_DATA' :
                this.db.execute("insert or replace into instance_app_data (app_id, instance_id, key_name, value) values (?, ?, ?,?)",
                        [this.appId, this.instanceId, request.key, request.value]);
                break;

            case 'FETCH_PERSON_APP_DATA' :
                var keyClause = opensocial.HappyHourContainer.keyNameClause(request.keys);
                var q = "select person_id, key_name, value from person_app_data where " + keyClause.sql +
                        " and app_id =  ?";
                var rs = null;
                switch (request.idSpec) {
                    case opensocial.DataRequest.PersonId.VIEWER:
                        q += " and person_id = ?"
                        rs = this.db.execute(q, keyClause.params.concat([this.appId, this.viewerId]));
                        break;
                    case opensocial.DataRequest.PersonId.OWNER:
                        q += " and person_id = ?"
                        rs = this.db.execute(q, keyClause.params.concat([this.appId, this.ownerId]));
                        break;
                    case opensocial.DataRequest.Group.VIEWER_FRIENDS:
                        q += " and person_id in " +
                             "(select friend_id from friends where person_id = ?)";
                        rs = this.db.execute(q, keyClause.params.concat([this.appId, this.viewerId]));
                        break;
                    case opensocial.DataRequest.Group.OWNER_FRIENDS:
                        q += " and person_id in " +
                             "(select friend_id from friends where person_id = ?)";
                        rs = this.db.execute(q, keyClause.params.concat([this.appId, this.ownerId]));
                        break;
                }
                if (rs) {
                    try {
                        var values = {};
                        while (rs.isValidRow()) {
                            var id = rs.field(0);
                            if (values[id]) {
                                values[id][rs.field(1)] = rs.field(2);
                            }
                            else {
                                values[id] = {};
                                values[id][rs.field(1)] = rs.field(2);
                            }
                            rs.next();
                        }
                        requestedValue = values;
                    } finally {
                        rs.close();
                    }
                }
                else {
                    requestedValue = {};
                }
                break;

            case 'UPDATE_PERSON_APP_DATA':
                var userId = request.id;
            
				// Gadgets can only edit viewer data
                if (userId == opensocial.DataRequest.PersonId.VIEWER
                        || userId == this.viewer.getId()) {
                    userId = this.viewer.getId();
                    this.db.execute("insert or replace into person_app_data (app_id, person_id, key_name, value) values (?, ?, ?,?)", [this.appId, userId, request.key, request.value]);
                } else {
                    hadError = true;
                }

                break;

            case 'FETCH_ACTIVITIES' :

                var q = "select\n" +
                        "a.app_id, a.user_id, a.stream_title,\n" +
                        "a.stream_favicon_url, a.stream_source_url, a.stream_url,\n" +
                        "a.id activity_id, a.title activity_title, a.summary activity_summary, \n" +
                        "a.body activity_body, a.external_id activity_external_id,\n" +
                        "a.url activity_url, ((strftime('%s', a.posted_time) -  strftime('%S', a.posted_time)) *1000 + strftime('%f', a.posted_time)*1000) activity_posted_time, a.extended_properties activity_extended_properties,\n" +
                        "m.id media_id, m.mime_type media_mime_type, m.type media_type, m.url media_url, m.extended_properties media_extended_properties\n" +
                        "from \n" +
                        "activity a \n" +
                        "left outer join activity_media_items m on m.activity_id = a.id\n" +
                        "where a.app_id = ? ";

                var rs = null;
                switch (request.idSpec) {
                    case opensocial.DataRequest.PersonId.VIEWER:
                        q += " and a.user_id = ? order by a.id, m.id"
                        rs = this.db.execute(q, [this.appId, this.viewerId]);
                        break;
                    case opensocial.DataRequest.PersonId.OWNER:
                        q += " and a.user_id = ? order by a.id, m.id"
                        rs = this.db.execute(q, [this.appId, this.ownerId]);
                        break;
                    case opensocial.DataRequest.Group.VIEWER_FRIENDS:
                        q += " and a.user_id in " +
                             "(select friend_id from friends where person_id = ?) order by a.id, m.id";
                        rs = this.db.execute(q, [this.appId, this.viewerId]);
                        break;
                    case opensocial.DataRequest.Group.OWNER_FRIENDS:
                        q += " and a.user_id in " +
                             "(select friend_id from friends where person_id = ?) order by a.id, m.id";
                        rs = this.db.execute(q, [this.appId, this.ownerId]);
                        break;
                }

                if (rs) {
                    try {
                        var activity = null;
                        var mediaItems = null;
                        var values = {};
                        var requestedActivities = [];
                        while (rs.isValidRow()) {
                            if (activity == null || activity.getId() != rs.fieldByName("activity_id")) {
                                if (activity != null && mediaItems != null) {
                                    activity.setField(opensocial.Activity.Field.MEDIA_ITEMS, mediaItems);
                                    mediaItems = null;
                                }
                                activity = this.materializeActivity(rs);
                                requestedActivities.push(activity);
                            }
                            if (rs.fieldByName("media_id")) {
                                var media = this.materializeMedia(rs);
                                if (mediaItems == null) {
                                    mediaItems = [media];
                                }
                                else {
                                    mediaItems.push(media);
                                }
                            }
                            rs.next();
                        }
                        if (activity != null && mediaItems != null) {
                            activity.setField(opensocial.Activity.Field.MEDIA_ITEMS, mediaItems);
                            mediaItems = null;
                        }
                        requestedValue = {
                            // Real containers should set the requested stream here
                            'activities' : new opensocial.Collection(requestedActivities)};
                    } finally {
                        rs.close();
                    }
                }
                else {
                    requestedValue = {};
                }
                break;
            default: 
                break;
        }

        dataResponseValues[requestName] = new opensocial.ResponseItem(request, requestedValue, hadError);
        globalError = globalError || hadError;
    }

    callback(new opensocial.DataResponse(dataResponseValues, globalError));
};

/**
 *@private
 */
opensocial.HappyHourContainer.prototype.materializePerson = function(rs) {
	var personId = rs.field(0);	
	var nameParms = {};
	nameParms[opensocial.Name.Field.UNSTRUCTURED] = rs.field(1);	
	var name = new opensocial.Name(nameParms);
	
	var fields = {
		'id' : personId,
		'name' : name,
		'thumbnailUrl' : rs.field(2),
		'profileUrl' : rs.field(3),
		'dateOfBirth' : rs.field(4),
		'gender' : rs.field(5),
		'email' : rs.field(6)
	};
	
	var xps  = rs.field(7);
	if(xps) {
		var extendedProperties = eval(xps);
		for(var key in extendedProperties) {
			fields[key] = extendedProperties[key];
		}
	}
	
	return new opensocial.Person(fields, personId == this.viewerId, personId == this.ownerId);
};

/**
 *@private
 */
opensocial.HappyHourContainer.prototype.materializeActivity = function(rs) {
    var activity = opensocial.newActivity({});
    
    activity.setField(opensocial.Activity.Field.TITLE, rs.fieldByName("activity_title"));
    activity.setField(opensocial.Activity.Field.ID, rs.fieldByName("activity_id"));
    activity.setField(opensocial.Activity.Field.APP_ID, rs.fieldByName("app_id"));
    activity.setField(opensocial.Activity.Field.USER_ID, rs.fieldByName("user_id"));

    activity.setField(opensocial.Activity.Field.SUMMARY, rs.fieldByName("activity_summary"));
    activity.setField(opensocial.Activity.Field.BODY, rs.fieldByName("activity_body"));
    activity.setField(opensocial.Activity.Field.EXTERNAL_ID, rs.fieldByName("activity_external_id"));
    activity.setField(opensocial.Activity.Field.URL, rs.fieldByName("activity_url"));
    activity.setField(opensocial.Activity.Field.POSTED_TIME, new Date(rs.fieldByName("activity_posted_time")));
    activity.setField(opensocial.Activity.Field.STREAM_TITLE, rs.fieldByName("stream_title"));
    activity.setField(opensocial.Activity.Field.STREAM_FAVICON_URL, rs.fieldByName("stream_favicon_url"));
    activity.setField(opensocial.Activity.Field.STREAM_SOURCE_URL, rs.fieldByName("stream_source_url"));
    activity.setField(opensocial.Activity.Field.STREAM_URL, rs.fieldByName("stream_source_url"));

    return activity;
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.materializeMedia = function(rs) {
    var opt_params = {
        'id' : rs.fieldByName("media_id"),
        'type' : rs.fieldByName("media_type")
    };
    
    var item = opensocial.newActivityMediaItem(rs.fieldByName("media_mime_type"), rs.fieldByName("media_url"), opt_params)
    
    return(item);
};

/**
 * @private
 */
opensocial.HappyHourContainer.prototype.toISO8601String = function (date) {
    var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; }

    var str = "";
    str += date.getUTCFullYear();
    str += "-" + zeropad(date.getUTCMonth() + 1);
    str += "-" + zeropad(date.getUTCDate());
    str += "T" + zeropad(date.getUTCHours()) +
               ":" + zeropad(date.getUTCMinutes())+
               ":" + zeropad(date.getUTCSeconds());
    var ms =  date.getUTCMilliseconds();
    str += "."+(ms < 10 ? "00"+ms : (ms < 100 ? "0"+ms : ms));
    return str;
};

/**
 * Request a profile for the specified person id.
 * When processed, returns a Person object.
 *
 * @param {String} id The id of the person to fetch. Can also be standard
 *    person IDs of VIEWER and OWNER.
 * @param {Map&lt;opensocial.DataRequest.PeopleRequestFields, Object&gt;}
 *    opt_params Additional params to pass to the request. This request supports
 *    PROFILE_DETAILS.
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newFetchPersonRequest = function(id,
                                                                         opt_params) {
    return {'type' : 'FETCH_PERSON', 'id' : id};
};

/**
 * Used to request friends from the server, optionally joined with app data
 * and activity stream data.
 * When processed, returns a Collection&lt;Person&gt; object.
 *
 * @param {Array&lt;String&gt; or String} idSpec An id, array of ids, or a group
 *    reference used to specify which people to fetch
 * @param {Map&lt;opensocial.DataRequest.PeopleRequestFields, Object&gt;}
 *    opt_params Additional params to pass to the request. This request supports
 *    PROFILE_DETAILS, SORT_ORDER, FILTER, FIRST, and MAX.
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newFetchPeopleRequest = function(idSpec,
                                                                         opt_params) {
    return {'type' : 'FETCH_PEOPLE', 'idSpec' : idSpec};
};

/**
 * Used to request global app data.
 * When processed, returns a Map&lt;String, String&gt; object.
 *
 * @param {Array&lt;String&gt;|String} keys The keys you want data for. This
 *     can be an array of key names, a single key name, or "*" to mean
 *     "all keys".
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newFetchGlobalAppDataRequest = function(keys) {
    return {
		'type' : 'FETCH_GLOBAL_APP_DATA', 
		'keys' : keys
	};
};

/**
 * Used to request instance app data.
 * When processed, returns a Map&lt;String, String&gt; object.
 *
 * @param {Array&lt;String&gt;|String} keys The keys you want data for. This
 *     can be an array of key names, a single key name, or "*" to mean
 *     "all keys".
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newFetchInstanceAppDataRequest = function(keys) {
    return {
		'type' : 'FETCH_INSTANCE_APP_DATA', 
		'keys' : keys
	};
};

/**
 * Used to request an update of an app instance field from the server.
 * When processed, does not return any data.
 *
 * @param {String} key The name of the key
 * @param {String} value The value
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newUpdateInstanceAppDataRequest = function(key, value) {
    return {
		'type' : 'UPDATE_INSTANCE_APP_DATA', 
		'key' : key, 
		'value' : value
	};
};

/**
 * Used to request app data for the given people.
 * When processed, returns a Map&lt;person id, Map&lt;String, String&gt;&gt;
 * object.
 *
 * @param {Array&lt;String&gt; or String} idSpec An id, array of ids, or a group
 *    reference. (Right now the supported keys are VIEWER, OWNER,
 *    OWNER_FRIENDS, or a single id within one of those groups)
 * @param {Array&lt;String&gt;|String} keys The keys you want data for. This
 *     can be an array of key names, a single key name, or "*" to mean
 *     "all keys".
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newFetchPersonAppDataRequest = function(idSpec, keys) {
    return {
		'type' : 'FETCH_PERSON_APP_DATA', 
		'idSpec' : idSpec, 
		'keys' : keys
	};
};

/**
 * Used to request an update of an app field for the given person.
 * When processed, does not return any data.
 *
 * @param {String} id The id of the person to update. (Right now only the
 *    special VIEWER id is allowed.)
 * @param {String} key The name of the key
 * @param {String} value The value
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newUpdatePersonAppDataRequest = function(id, key, value) {
    return {
		'type' : 'UPDATE_PERSON_APP_DATA', 
		'id' : id, 
		'key' : key,
        'value' : value
	};
};

/**
 * Used to request an activity stream from the server.
 * Note: Although both app and folder are optional, you can not just provide a
 * folder.
 * When processed, returns an object where "activities" is a
 * Collection&lt;Activity&gt; object and "requestedStream" is a Stream object
 * representing the stream you fetched. (Note: this may or may not be different
 * that the streams that each activity belongs to)
 *
 * @param {Array&lt;String&gt; or String} idSpec An id, array of ids, or a group
 *  reference to fetch activities for
 * @param {Map&lt;opensocial.DataRequest.ActivityRequestFields, Object&gt;}
 *    opt_params Additional params to pass to the request.
 * @return {Object} a request object
 */
opensocial.HappyHourContainer.prototype.newFetchActivitiesRequest = function(idSpec, opt_params) {
    return {'type' : 'FETCH_ACTIVITIES', 'idSpec' : idSpec};
};

/**
 * @return {opensocial.Environment} returns the current container's Environment
 */
opensocial.HappyHourContainer.prototype.getEnvironment = function() {
  
    var supportedFields = {
        'person' : {
          'id' : true,
          'name' : true,
          'thumbnailUrl' : true,
          'profileUrl' : true,
          'gender' : true,
          'dateOfBirth' : true,
          'email' : true
        },
        'activity' : {
          'id' : true,
          'externalId' : true,
          'userId' : true,
          'appId' : true,
          'streamTitle' : true,
          'streamUrl' : true,
          'streamSourceUrl' : true,
          'streamFaviconUrl' : true,
          'title' : true,
          'body' : true,
          'url' : true,
          'mediaItems' : true,
          'postedTime' : true,
          'customValues' : false
        },
        'activityMediaItem' : {
          'type' : true,
          'mimeType' : true,
          'url' : true
        }
    };

    var host = 'google.com';
    try { host = window.location.host; } catch (e){}
    var environment = this.newEnvironment(host, supportedFields);

    // In a real container this environment will probably be static
    return (environment);
};

/**
 * @return {boolean} true, if the current gadget has access to the specified permission.
 */
opensocial.HappyHourContainer.prototype.hasPermission = function(permission) {
  // The sample container always grants access to the viewer object
  return permission == opensocial.Permission.VIEWER;
};

/**
 *  
 */
opensocial.HappyHourContainer.prototype.requestPermission = function(permissions,
    reason, opt_callback) {
  // TODO(doll): We should probably add a permission restriction to the sample
  // container just for the sake of providing a reference implementation.
  if (opt_callback) {
    opt_callback();
  }
}; 

/**
 *  
 */
opensocial.HappyHourContainer.prototype.requestShareApp = function(recipients, reason, opt_callback) {
    var resp = new opensocial.ResponseItem();
    if (opt_callback) {
        opt_callback(resp);
    }
}; 